package com.hmdp.core.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.hmdp.common.constant.RedisConstants;
import com.hmdp.utils.RedisData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * @author 计算机系 ITAEM 摆渡人
 * @description
 * @date 2022/10/15 21:24
 */
@Slf4j
@Component
public class RedisServiceImpl {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final String DEFAULT_KEY_PREFIX = "";
    private final int EXPIRE_TIME = 1;
    private final TimeUnit EXPIRE_TIME_TYPE = TimeUnit.DAYS;

    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 对象数据缓存至redis
     * @param key 键
     * @param value 值
     */
    public <K, V> void set(K key, V value) {
        try {
            if (value != null) {
                stringRedisTemplate.opsForValue().set(DEFAULT_KEY_PREFIX + key, JSON.toJSONString(value));
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException("数据缓存至redis失败");
        }
    }

    /**
     * 对象数据缓存至redis并设置过期时间
     * @param key 键
     * @param value 值
     */
    public <K, V> void set(K key, V value, long timeout, TimeUnit unit) {
        try {
            if (value != null) {
                stringRedisTemplate.opsForValue().set(DEFAULT_KEY_PREFIX + key, JSON.toJSONString(value), timeout,
                        unit);
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException("数据缓存至redis失败");
        }
    }

    /**
     * 字符串数据缓存至redis
     * @param key 键
     * @param value 值
     */
    public void set(String key,String value){
        try {
            if(StringUtils.isNotBlank(value)){
                stringRedisTemplate.opsForValue().set(key,value);
            }
        } catch (Exception e){
            log.error(e.getMessage(),e);
            throw new RuntimeException("数据缓存至redis失败");
        }
    }

    /**
     * 字符串数据缓存至redis并设置过期时间
     * @param key
     * @param value
     * @param timeout
     * @param unit
     */
    public void set(String key,String value,long timeout, TimeUnit unit){
        try {
            if(StringUtils.isNotBlank(value)){
                stringRedisTemplate.opsForValue().set(key,value,timeout,unit);
            }
        } catch (Exception e){
            log.error(e.getMessage(),e);
            throw new RuntimeException("数据缓存至redis失败");
        }
    }

    /**
     * 对象数据缓存至redis，已存在则不缓存
     * @param key
     * @param value
     * @param <K>
     * @param <V>
     * @return
     */
    public <K,V> boolean setNx(K key,V value){
        return Boolean.TRUE.equals(stringRedisTemplate
                .opsForValue()
                .setIfAbsent(DEFAULT_KEY_PREFIX + key, JSON.toJSONString(value)));
    }

    /**
     * 对象数据缓存至redis并设置过期时间，已存在则不缓存
     * @param key
     * @param value
     * @param timeOut
     * @param unit
     * @param <K>
     * @param <V>
     * @return
     */
    public <K,V> boolean setNx(K key,V value,Long timeOut,TimeUnit unit){
        return Boolean.TRUE.equals(stringRedisTemplate
                .opsForValue()
                .setIfAbsent(DEFAULT_KEY_PREFIX + key, JSON.toJSONString(value),timeOut,unit));
    }

    /**
     * 字符串数据缓存至redis，已存在则不缓存
     * @param key
     * @param value
     * @return
     */
    public boolean setNx(String key,String value){
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, value));
    }

    /**
     * 字符串数据缓存至redis并设置过期时间，已存在则不缓存
     * @param key
     * @param value
     * @param timeout
     * @param unit
     * @return
     */
    public boolean setNx(String key,String value,long timeout,TimeUnit unit){
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit));
    }

    /**
     * 缓存穿透
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param timeout
     * @param unit
     * @param <R>
     * @param <K>
     * @return
     */
    public <R,K> R queryWithPassThrough(String keyPrefix, K id, Class<R> type, Function<K,R> dbFallback,long timeout,TimeUnit unit) {
        String key = keyPrefix + id;
        //从redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //判断是否存在
        if(StringUtils.isNotBlank(json)){
            //存在，直接返回
            return JSON.parseObject(json,type);
        }
        // 判断命中的是否为空值,""不是空值，证明数据不存在
        if(json != null){
            return null;
        }

        //不存在，去数据库查
        R r = dbFallback.apply(id);
        //不存在，返回错误
        if(r == null){
            //将空值写入redis
            stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            //返回错误信息
            return null;
        }
        //存在，写入redis
        this.set(key,r,timeout,unit);
        return r;
    }

    /**
     * 逻辑过期
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param timeout
     * @param unit
     * @param <R>
     * @param <K>
     * @return
     */
    public <R,K> R queryWithLogicExpire(String keyPrefix,K id,Class<R> type,Function<K,R> dbFallback,long timeout,TimeUnit unit) {
        //1.从redis查询数据
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);

        //判断是否存在
        if(StringUtils.isBlank(json)){
            return null;
        }

        //json反序列化成对象
        RedisData redisData = JSON.parseObject(json, RedisData.class);
        R r = JSON.parseObject(String.valueOf(redisData.getData()), type);
        LocalDateTime expireTime = redisData.getExpireTime();

        //2.存在，判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //2.1未过期，直接返回
            return r;
        }
        //2.2已过期，缓存重建
        //3.缓存重建
        //3.1获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        if(isLock){
            //3.2成功，开启独立线程进行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R rr = dbFallback.apply(id);
                    //写入redis
                    this.addWithLogicalExpire(key,rr,timeout,unit);
                } catch (Exception e){
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }

        //3.3失败，直接返回过期商品信息
        return r;
    }

    /**
     * 设置逻辑过期时间
     * @param key
     * @param value
     * @param time
     * @param unit
     * @param <K>
     * @param <V>
     */
    public <K,V> void addWithLogicalExpire(String key,V value,Long time,TimeUnit unit){
        //设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        //写入redis
        try {
            if (value != null) {
                stringRedisTemplate.opsForValue().set(DEFAULT_KEY_PREFIX + key, JSON.toJSONString(redisData));
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException("数据缓存至redis失败");
        }
    }

    /**
     * 互斥锁
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param <R>
     * @param <K>
     * @return
     */
    public <R,K> R queryWithMutex(String keyPrefix,K id,Class<R> type,Function<K,R> dbFallback) {
        //从redis查询数据
        String key = keyPrefix + id;

        String json = stringRedisTemplate.opsForValue().get(key);
        if(StringUtils.isNotBlank(json)){
            return JSON.parseObject(json,type);
        }
        if(json != null){
            return null;
        }

        //1.实现缓存重建互斥锁
        //1.1.获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        try {
            boolean isLock = tryLock(lockKey);
            //1.2.判断是否获取成功
            if (!isLock) {
                //不成功则休眠，并重新尝试
                Thread.sleep(50);
                return queryWithMutex(keyPrefix,id,type,dbFallback);
            }
            //1.3.成功，根据id查询数据库
            R r = dbFallback.apply(id);
            if (Objects.isNull(r)) {
                //保存空值，针对缓存穿透问题
                this.set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //1.4.写入redis
            this.set(key, r, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return r;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //1.5.释放锁
            unlock(lockKey);
        }
    }

    /**
     * 上锁
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        return this.setNx(key, "lock", 10, TimeUnit.SECONDS);
    }

    /**
     * 解锁
     * @param key
     */
    private void unlock(String key) {
        this.delete(key);
    }



    /**
     * 写入 hash-set,已经是key-value的键值，不能再写入为hash-set
     *
     * @param key    must not be {@literal null}.
     * @param subKey must not be {@literal null}.
     * @param value  写入的值
     */
    public <K, SK, V> void addHashCache(K key, SK subKey, V value) {
        stringRedisTemplate.opsForHash().put(DEFAULT_KEY_PREFIX + key, subKey, value);
    }

    /**
     * 写入 hash-set,并设置过期时间
     *
     * @param key    must not be {@literal null}.
     * @param subKey must not be {@literal null}.
     * @param value  写入的值
     */
    public <K, SK, V> void addHashCache(K key, SK subKey, V value, long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForHash().put(DEFAULT_KEY_PREFIX + key, subKey, value);
        stringRedisTemplate.expire(DEFAULT_KEY_PREFIX + key, timeout, unit);
    }

    /**
     * 将对象以Hash的形式存入redis
     *
     * @param key
     * @param value
     * @param <K>
     * @param <V>
     */
    public <K, V> void putAll(K key, V value) {
        Map<String, Object> map = BeanUtil.beanToMap(value,new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
        stringRedisTemplate.opsForHash().putAll(DEFAULT_KEY_PREFIX + key, map);
    }

    /**
     * 将对象以Hash的形式存入redis
     *
     * @param key
     * @param value
     * @param <K>
     * @param <V>
     */
    public <K, V> void putAll(K key, V value, long timeout, TimeUnit timeUnit) {
        Map<String, Object> map = BeanUtil.beanToMap(value,new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
        stringRedisTemplate.opsForHash().putAll(DEFAULT_KEY_PREFIX + key, map);
        stringRedisTemplate.expire(DEFAULT_KEY_PREFIX + key, timeout, timeUnit);
    }

    /**
     * 获取hash值，并包装成一个对象放回
     * @param key
     * @param clazz
     * @param <K>
     * @param <V>
     * @return
     */
    public <K, V> V getHashObject(K key, V clazz) {
        Map<Object, Object> objectMap = stringRedisTemplate.opsForHash().entries(DEFAULT_KEY_PREFIX + key);
        V result = null;
        if (!objectMap.isEmpty()) {
            result = BeanUtil.fillBeanWithMap(objectMap, clazz, false);
        }
        return result;
    }

    /**
     * 获取 hash-setvalue
     *
     * @param key    must not be {@literal null}.
     * @param subKey must not be {@literal null}.
     */
    public <K, SK> Object getHashCache(K key, SK subKey) {
        return stringRedisTemplate.opsForHash().get(DEFAULT_KEY_PREFIX + key, subKey);
    }


    /**
     * 从redis中获取缓存数据，转成对象
     *
     * @param key   must not be {@literal null}.
     * @param clazz 对象类型
     * @return
     */
    public <K, V> V getObject(K key, Class<V> clazz) {
        String value = this.get(key);
        V result = null;
        if (!StringUtils.isEmpty(value)) {
            result = JSONObject.parseObject(value, clazz);
        }
        return result;
    }

    /**
     * 从redis中获取缓存数据，转成list
     *
     * @param key   must not be {@literal null}.
     * @param clazz 对象类型
     * @return
     */
    public <K, V> List<V> getList(K key, Class<V> clazz) {
        String value = this.get(key);
        List<V> result = new ArrayList<>();
        if (!StringUtils.isEmpty(value)) {
            result = JSONArray.parseArray(value, clazz);
        }
        return result;
    }

    /**
     * 功能描述：Get the value of {@code key}.
     *
     * @param key must not be {@literal null}.
     * @return java.lang.String
     * @date 2021/9/19
     **/
    public <K> String get(K key) {
        String value;
        try {
            value = stringRedisTemplate.opsForValue().get(DEFAULT_KEY_PREFIX + key);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException("从redis缓存中获取缓存数据失败");
        }
        return value;
    }

    /**
     * 删除key
     */
    public void delete(String key) {
        stringRedisTemplate.delete(key);
    }

    /**
     * 批量删除key
     */
    public void delete(Collection<String> keys) {
        stringRedisTemplate.delete(keys);
    }

    /**
     * 序列化key
     */
    public byte[] dump(String key) {
        return stringRedisTemplate.dump(key);
    }

    /**
     * 是否存在key
     */
    public Boolean hasKey(String key) {
        return stringRedisTemplate.hasKey(key);
    }

    /**
     * 设置过期时间
     */
    public Boolean expire(String key, long timeout, TimeUnit unit) {
        return stringRedisTemplate.expire(key, timeout, unit);
    }

    /**
     * 设置过期时间
     */
    public Boolean expireAt(String key, Date date) {
        return stringRedisTemplate.expireAt(key, date);
    }


    /**
     * 移除 key 的过期时间，key 将持久保持
     */
    public Boolean persist(String key) {
        return stringRedisTemplate.persist(key);
    }

    /**
     * 返回 key 的剩余的过期时间
     */
    public Long getExpire(String key, TimeUnit unit) {
        return stringRedisTemplate.getExpire(key, unit);
    }

    /**
     * 返回 key 的剩余的过期时间
     */
    public Long getExpire(String key) {
        return stringRedisTemplate.getExpire(key);
    }
}